/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package net.worlds;

import java.awt.Color;
import net.worlds.object.Steppable;
import net.worlds.ui.PartTreeNode;
import net.worlds.ui.UI;
import net.worlds.object.Prop;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.worlds.lua.Event;
import net.worlds.net.RemotePlayer;
import net.worlds.object.AngularVelocityController;
import net.worlds.object.Box;
import net.worlds.object.Circle;
import net.worlds.object.Environment;
import net.worlds.object.FixedRotation;
import net.worlds.object.GameObject;
import net.worlds.object.HiddenScript;
import net.worlds.object.Hinge;
import net.worlds.object.ImageEffect;
import net.worlds.object.Inventory;
import net.worlds.object.Model;
import net.worlds.object.Part;
import net.worlds.object.Player;
import net.worlds.object.Root;
import net.worlds.object.Rope;
import net.worlds.object.Script;
import net.worlds.object.StarterInventory;
import net.worlds.object.Tool;
import net.worlds.object.VelocityController;
import net.worlds.object.Weld;
import org.jbox2d.collision.AABB;
import org.jbox2d.collision.shapes.Shape;
import org.jbox2d.common.Vec2D;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.PhysicsWorld;
import org.jbox2d.dynamics.joints.JointDef;

/**
 *
 * @author Jacob Morgan
 */
public final class World {
    public Player localPlayer;
    public String name;
    public Game game;
    public boolean destroyed = false;
    public Environment environment = null;

    public boolean isStepping = false;

    private List<GameObject> objects;
    private ArrayList<GameObject> steppableObjects;
    private final HashMap<Class, List<GameObject>> objectIndex;
    private Map<Integer, List<GameObject>> objectLayers = Collections.synchronizedMap(new HashMap<Integer, List<GameObject>>(3));

    private float width;
    private float height;
    private PhysicsWorld physicsWorld;
    private Root rootObject;
    private Color backgroundColor = new Color(220, 220, 220);
    private BufferedImage backgroundImage;
    public boolean scrollBackground = false;
    public int physicsAccuracy;
    private boolean controlPhysicsFPS;
    private long time = System.currentTimeMillis();
    private float accumulator = 0;
    private float dt = 1/(Main.fps);
    private float timeScale = 1;
    private int elapsedTime = 0;
    private int step;
    private int numSteps = 1;
    public double fps;
    private static String message = null;

    public long frame = 0;
    public boolean scaleBackground = false;

    private HashMap<Body, Integer> serializedBodies;
    private ArrayList<PartConnection> partConnections = new ArrayList<PartConnection>(WorldsSettings.MAX_OBJECTS);
    private XContactFilter contactFilter;

    public ImageEffect effect;

    private ArrayList<Part> serializedParts;
    public World(int maxObjects) throws WorldsException
    {
        //new Throwable().printStackTrace();
        objects = Collections.synchronizedList(new ArrayList<GameObject>(maxObjects));
        steppableObjects = new ArrayList<GameObject>(maxObjects);
        objectIndex = new HashMap<Class, List<GameObject>>(WorldsSettings.MAX_TYPES);
        for(int i=0; i < 3; i++)
            objectLayers.put(i, Collections.synchronizedList(new ArrayList<GameObject>(WorldsSettings.MAX_TYPES)));
        AABB worldAABB = new AABB();
        worldAABB.lowerBound = new Vec2D((float) 0, 0);
        worldAABB.upperBound = new Vec2D(1, 1);
        Vec2D gravity = new Vec2D(0, -8);
        physicsWorld = new PhysicsWorld(worldAABB, gravity, true);
        physicsWorld.setContactListener(new XContactListener());
        contactFilter = new XContactFilter();
        physicsWorld.setContactFilter(contactFilter);
        physicsAccuracy = Settings.getInt("PhysicsAccuracy");
        controlPhysicsFPS = Settings.getBoolean("ControlPhysicsFPS");
        org.jbox2d.common.Settings.clearForces = !controlPhysicsFPS;
        for(Class c : ObjectList.getAllClasses())
        {
            initializeObjectIndex(c);
        }
    }
    public XContactFilter getContactFilter()
    {
        return contactFilter;
    }
    public Body getBodyList()
    {
        return physicsWorld.getBodyList();
    }
    public void setSize(final float width, final float height) throws WorldsException
    {
        this.width = width;
        this.height = height;
        AABB worldAABB = new AABB();
        worldAABB.lowerBound = new Vec2D((float) -width, (float) -height);
        worldAABB.upperBound = new Vec2D((float) width, (float) height);
        physicsWorld.setAABB(worldAABB);
        if(Main.isClient())
            return;
    }
    public void buildBoundaries()
    {
       try {
        net.worlds.object.World world = (net.worlds.object.World) getService("World");
            if(world.hasChild("Boundaries"))
                world.getChild("Boundaries").remove();

            Model m = (Model) createObject("Model");
            m.setParent(world);
            m.setProp("Name", "Boundaries", true);

            Box top = (Box) createObject("Box");
            top.setProp("Size", new Vec2D(width, 0.5f), true);
            top.setProp("Position", new Vec2D(width/2, height - 0.25f), true);
            top.setProp("Fixed", true, true);
            top.setProp("Name", "TopBoundary", true);
            top.setProp("ShowBorder", false, true);
            top.setProp("Transparency", 0.7f, true);
            top.setParent(m);
            Box bottom = (Box) createObject("Box");
            bottom.setProp("Size", new Vec2D(width, 0.5f), true);
            bottom.setProp("Position", new Vec2D(width/2, 0.25f), true);
            bottom.setProp("Fixed", true, true);
            bottom.setProp("Name", "BottomBoundary", true);
            bottom.setProp("ShowBorder", false, true);
            bottom.setProp("Transparency", 0.7f, true);
            bottom.setParent(m);
            Box left = (Box) createObject("Box");
            left.setProp("Size", new Vec2D(0.5f, height), true);
            left.setProp("Position", new Vec2D(0.25f, height/2), true);
            left.setProp("Fixed", true, true);
            left.setProp("Name", "LeftBoundary", true);
            left.setProp("ShowBorder", false, true);
            left.setProp("Transparency", 0.7f, true);
            left.setParent(m);
            Box right = (Box) createObject("Box");
            right.setProp("Size", new Vec2D(0.5f, height), true);
            right.setProp("Position", new Vec2D(width-0.25f, height/2), true);
            right.setProp("Fixed", true, true);
            right.setProp("Name", "RightBoundary", true);
            right.setProp("ShowBorder", false, true);
            right.setProp("Transparency", 0.7f, true);
            right.setParent(m);
            Box ground = (Box) createObject("Box");
            ground.setProp("Size", new Vec2D(width, 40), true);
            ground.setProp("Position", new Vec2D(width / 2, 20), true);
            ground.setProp("Fixed", true, true);
            ground.setProp("Name", "Ground", true);
            ground.setProp("ShowBorder", false, true);
            ground.setProp("Color", new Color(41, 97, 49), true);
            ground.setParent(world);
        } catch (WorldsException ex) {
            ex.printStackTrace();
        }
    }
    public void resetSerializedBodies()
    {
        serializedBodies = new HashMap<Body, Integer>(WorldsSettings.MAX_OBJECTS);
        serializedParts = new ArrayList<Part>(WorldsSettings.MAX_OBJECTS);
    }
    public void setGravity(Vec2D v)
    {
        physicsWorld.setGravity(v);
        for(Body body = physicsWorld.getBodyList(); body != null; body = body.getNext())
        {
            body.wakeUp();
        }
    }
    @Override
    public World clone()
    {
        try {
            World w = new World(objects.size());
            for (GameObject obj : objects) {
                w.addObject((GameObject) obj.clone());
            }
            return w;
        } catch (WorldsException ex) {
            Main.error(ex);
            return null;
        }
    }
    public String serializeToString() throws IOException, WorldsException
    {
        serializedBodies = new HashMap<Body, Integer>(WorldsSettings.MAX_OBJECTS);
        Writer w = new StringWriter();
        w.write("game:setID("+rootObject.getID()+")\n");
        w.write("game:setType("+game.type+")\n");
        w.write("function c(p) return create(p) end\n");
        serializeObjectRecursive(w, rootObject, false, true);
        w.flush();
        return w.toString();
    }
    public void serialize(OutputStream os, boolean urlencode) throws IOException, WorldsException
    {
        serializedBodies = new HashMap<Body, Integer>(WorldsSettings.MAX_OBJECTS);
        Writer w;
        if(urlencode)
            w = new URLEncodingWriter(os);
        else
            w = new OutputStreamWriter(os);
        w.write("game:setType("+game.type+")\n");
        w.write("function c(p) return create(p) end\n");
        serializeObjectRecursive(w, rootObject);
        w.flush();
    }
    public void serializeObjectRecursive(Writer w, GameObject obj) throws IOException, WorldsException
    {
        serializeObjectRecursive(w, obj, false, false);
    }
    public void serializeObjectRecursive(Writer w, GameObject obj, boolean serializeThis, boolean setIDs) throws IOException, WorldsException
    {
        if(!Util.isPersistable(obj.getType()) && !obj.getStringProp("Name").equals("DefaultCharacter"))
            return;
        if(serializeThis)
            serializeObject(w, obj, serializeThis);
        List<GameObject> children = obj.getChildren();
        synchronized(children)
        {
            for(GameObject c : obj.getChildren())
            {
                serializeObject(w, c, false, setIDs);
                serializeObjectRecursive(w, c, false, setIDs);
            }
        }
    }
    public void serializeObject(Writer w, GameObject obj, boolean setIDs) throws IOException, WorldsException
    {
        serializeObject(w, obj, false, setIDs);
    }
    private void serializeObject(Writer w, GameObject obj, boolean fixParent, boolean setIDs) throws IOException, WorldsException
    {
        if(!Util.isPersistable(obj.getType()) && !obj.getStringProp("Name").equals("DefaultCharacter"))
            return;
        String name = "p"+obj.getID();
        if(obj.getParent() == rootObject)
            w.write(name+"=game:getService(\""+obj.getType()+"\")\n");
        else
            w.write(name+"=c(\""+obj.getType()+"\")\n");
        if(setIDs)
            w.write(name+":setID("+obj.getID()+")\n");
        if(obj instanceof Part && ((Part)obj).getBody() != null)
        {
            Part p = (Part) obj;
            if(p.getBody().getParts().length > 1 && serializedBodies.containsKey(p.getBody()))
            {
                w.write(name+":setBody(p"+serializedBodies.get(p.getBody())+":getID())\n");
            }
            else
            {
                serializedBodies.put(p.getBody(), p.getID());
            }
            if(serializedParts != null)
                serializedParts.add(p);
        }
        for(String k : obj.getAllProperties().keySet())
        {
            if(!k.equals("Parent") && obj.getProp(k) != null && !(k.equals("Mass") && ((Part)obj).getBody().customMass == 0))
            {
                if(ObjectList.getPropDef(obj.getClass().getSimpleName(), k).scriptEditable && !(ObjectList.getPropDef(obj.getClass().getSimpleName(), k).defaultValue != null && ObjectList.getPropDef(obj.getClass().getSimpleName(), k).defaultValue.equals(obj.getProp(k))))
                    serializeProp(w, obj, name, k, obj.getProp(k));
            }
        }
        if(obj.getParent() != rootObject )
        {
            if(!fixParent)
                serializeProp(w, obj, name, "Parent", obj.getParent());
            else
                w.write("p"+obj.getID()+".Parent = game:get($PARENT)\n");
        }
    }
    private void serializeProp(Writer w, GameObject obj, String name, String k, Object v) throws IOException
    {
        String value = null;
        if(v == rootObject)
            value = "game";
        else if(v instanceof GameObject)
        {
            GameObject object = (GameObject) v;
            try {
                if (object.getParent() == object.world.rootObject) {
                    value = "game:getService(\"" + object.getType() + "\")";
                }
                else {
                    value = "p"+((GameObject)v).getID();
                }
                    
            } catch (WorldsException ex) {
                value = "nil";
            }
        }
        else if(v instanceof Vec2D)
            value = "Vec2D("+((Vec2D)v).x+", "+((Vec2D)v).y+")";
        else if(v instanceof Color)
            value = "Color("+Util.colorToString((Color)v)+")";
        else if(v instanceof String)
            value = "[["+((String)v).replace("[", "\\[\\").replace("]", "\\]\\")+"]]";
        else
            value = v.toString();
        w.write(name+"."+k+"="+value+"\n");
    }
    public ArrayList<Part> getSerializedParts()
    {
        return serializedParts;
    }
    public void rebuild()
    {
        try {
            rebuild(rootObject);
        } catch (WorldsException ex) {
            Main.error(ex);
        }
    }
    private void rebuild(GameObject obj) throws WorldsException
    {
        for(String k : obj.getAllProperties().keySet())
        {
            obj.setProp(k, obj.getProp(k), true);
        }
        List<GameObject> children = obj.getChildren();
        synchronized(children)
        {
            for(GameObject c : children)
            {
                rebuild(c);
            }
        }
    }
    public GameObject createObject(String type)
    {
        return createObject(type, -1, null, true);
    }
    public GameObject createObject(String type, int oldID, int id, RemotePlayer ignorePlayer)
    {
        game.changedIDs.put(oldID, id);
        return createObject(type, id, ignorePlayer, true);
    }
    public GameObject createObject(String type, int id, RemotePlayer ignorePlayer, boolean replicate)
    {
        try {
            GameObject obj = (GameObject) Class.forName("net.worlds.object."+type).newInstance();
            obj.world = this;
            obj.setID();
            if(id != -1)
                obj.setID(id, false);
            obj.setDefaultProperties();
            if(replicate)
            {
                obj.replicate(ignorePlayer);
                obj.replicateClient();
            }
            return obj;
        } catch (Exception ex) {
            //if(Thread.currentThread() != Main.mainThread)
                ex.printStackTrace();
            throw new RuntimeException("Unknown object type "+type);
        }
    }
    public void initializeObjectIndex(Class type)
    {
        objectIndex.put(type, new ArrayList<GameObject>(WorldsSettings.MAX_OBJECTS));
    }
    public List<GameObject> getObjectsByType(Class type)
    {
        return objectIndex.get(type);
    }
    public List<GameObject> getParts()
    {
        List<GameObject> boxes = objectIndex.get(Box.class);
        List<GameObject> circles = objectIndex.get(Circle.class);
        synchronized(boxes)
        {
            List<GameObject> r = boxes;
            synchronized(circles)
            {
                r.addAll(circles);
                return r;
            }
        }
    }
    public List<GameObject> getJoints()
    {
        List<GameObject> boxes = objectIndex.get(Hinge.class);
        List<GameObject> circles = objectIndex.get(Weld.class);
        List<GameObject> ropes = objectIndex.get(Rope.class);
        synchronized(boxes)
        {
            List<GameObject> r = boxes;
            synchronized(circles)
            {
                r.addAll(circles);
                synchronized(ropes)
                {
                    r.addAll(ropes);
                }
                return r;
            }
        }
    }
    public List<GameObject> getLayer(int i)
    {
        return objectLayers.get(i);
    }
    public List<GameObject> getLayerOriginal(int i)
    {
        return objectLayers.get(i);
    }
    public float getWidth()
    {
        return width;
    }
    public float getHeight()
    {
        return height;
    }
    public Color getSkyColor()
    {
        return backgroundColor;
    }
    public void addObject(final GameObject obj) throws WorldsException
    {
        Util.checkThread();
        _addObject(obj);
    }
    public void _addObject(GameObject obj) throws WorldsException
    {
        synchronized(objects)
        {
            objects.add(obj);
        }
        if(obj instanceof Steppable)
            steppableObjects.add(obj);
        synchronized(objectIndex.get(obj.getClass()))
        {
            objectIndex.get(obj.getClass()).add(obj);
        }
        if(obj instanceof Prop)
        {
            List<GameObject> layer = objectLayers.get(obj.getIntProp("Layer"));
            synchronized(layer)
            {
                layer.add(obj);
            }
        }
        obj.addToWorld();
    }
    public void changeLayer(GameObject obj, int oldLayerID, int newLayerID) throws WorldsException
    {
        List<GameObject> oldLayer = objectLayers.get(oldLayerID);
        synchronized(oldLayer)
        {
            oldLayer.remove(obj);
        }
        List<GameObject> newLayer = objectLayers.get(newLayerID);
        synchronized(oldLayer)
        {
            newLayer.add(obj);
        }
    }
    public void deleteObject(final GameObject obj, final boolean doDestructor) throws WorldsException
    {
        _deleteObject(obj, doDestructor);
    }
    public void deleteObject(final GameObject obj) throws WorldsException
    {
        Util.checkThread();
        deleteObject(obj, true);
    }
    private void _deleteObject(GameObject obj, final boolean doDestructor) throws WorldsException
    {
        //new Throwable().printStackTrace();
        if(!obj.isInWorld())
            return;
        if(obj.getParent() != null)
        {
            //obj.getParent().removeChild(obj);
            if(obj.world == Main.getGame().getCurrentWorld() && obj.getTreeNode() != null && obj.getTreeNode().getParent() != null)
                UI.onChildRemoved(((PartTreeNode)obj.getTreeNode().getParent()).getObject(), obj);
        }
        if(obj instanceof Steppable)
            steppableObjects.remove(obj);
        synchronized(objects)
        {
            objects.remove(obj);
        }
        synchronized(game.objectIDs)
        {
            game.objectIDs.put(new Integer(obj.getID()), null);
        }
        synchronized(objectIndex.get(obj.getClass()))
        {
            objectIndex.get(obj.getClass()).remove(obj);
        }
        synchronized(game.freeIDs)
        {
            if(!game.freeIDs.contains(obj.getID()))
                game.freeIDs.add(obj.getID());
        }
        if(obj instanceof Prop)
        {
            List<GameObject> layer = objectLayers.get(obj.getIntProp("Layer"));
            synchronized(layer)
            {
                layer.remove(obj);
            }
        }
        obj.getEvent("Removed").fire(Event.NULL);
        for(final GameObject c : obj.getChildren())
            if(c != null)
                _deleteObject(c, doDestructor);
        if(doDestructor)
            obj.destructor();
    }
    private void _deleteObject(GameObject obj) throws WorldsException
    {
        _deleteObject(obj, true);
    }
    public List<GameObject> getObjects()
    {
        return objects;
    }
    public Root getRootObject()
    {
        return rootObject;
    }
    public Body addBody(BodyDef bd)
    {
        Util.checkThread();
        Body body = physicsWorld.createBody(bd);
        body.world = this;
        return body;
    }
    public void removeBody(Body b)
    {
        Util.checkThread();
        if(!b.m_isDead)
            physicsWorld.destroyBody(b);
    }
    public org.jbox2d.dynamics.joints.Joint addJoint(JointDef j)
    {
        Util.checkThread();
        return physicsWorld.createJoint(j);
    }
    public void destroyJoint(org.jbox2d.dynamics.joints.Joint j)
    {
        Util.checkThread();
        physicsWorld.destroyJoint(j);
    }
    public Body getGroundBody()
    {
        return physicsWorld.getGroundBody();
    }
    public void create() throws WorldsException
    {
        rootObject = (Root) createObject("Root");
        _addObject(rootObject);
        /*net.worlds.object.World weld = new net.worlds.object.World();
        weld.setProp("Parent", rootObject, true);
        JointsService j = new JointsService();
        j.setProp("Parent", rootObject, true);
        Camera c = new Camera();
        c.setProp("Parent", rootObject, true);
        weld.setProp("Size", new Vec2D(1000, 500), true);
        Players p = new Players();
        p.setProp("Parent", rootObject, true);
        TaskScheduler t = new TaskScheduler();
        t.setProp("Parent", rootObject, true);*/
    }
    public void setup(int width, int height)
    {
        String scr = Util.getTextFromResource("/startup.lua").replace("$WORLD", name).replace("$WIDTH", ""+width).replace("$HEIGHT", ""+height);
        Main.getScriptContext().executeScript(scr);
    }
    public void destroy() throws WorldsException
    {
        destroyed = true;
        objects.clear();
        this.objectIndex.clear();
        this.objectLayers.clear();
        physicsWorld.destroy();
        rootObject.remove();
    }
    public GameObject getService(final String name) throws WorldsException
    {
        if(rootObject == null)
            return null;
        if(rootObject.hasChild(name))
            return rootObject.getChild(name);
        else
        {
            if(Thread.currentThread() == Main.mainThread)
            {
                GameObject obj = createObject(name);
                obj.setParent(rootObject);
                return obj;
            }
            else
            {
                Runnable r = new Runnable()
                {
                    public void run() {
                        try {
                            GameObject obj = createObject(name);
                            obj.setParent(rootObject);
                        } catch (WorldsException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                };
                Main.runInMainThread(r);
                return null;
            }
        }
    }
    public void stop()
    {
        time = System.currentTimeMillis();
        rootObject.getEvent("Stopped").fire(Event.NULL);
    }
    public void play()
    {
        time = System.currentTimeMillis();
        rootObject.getEvent("Started").fire(Event.NULL);
    }
    public Shape[] query(AABB aabb, int maxCount)
    {
        return physicsWorld.query(aabb, 32768);
    }
    public void step0()
    {
        physicsWorld.step(0, 1);
    }
    public void step() throws WorldsException
    {
        if(game.isPlaying)
        {
            long newTime = System.nanoTime();
            int frameTime = (int) Math.round((newTime - time)/1000000f);
            fps = 1d/(frameTime/1000f);
            if(controlPhysicsFPS)
            {
                if(frameTime > 300)
                    frameTime = 300;
                elapsedTime = frameTime;
                time = newTime;
                accumulator += frameTime / 1000f;
                if(accumulator < 0)
                    accumulator = 0;

                numSteps = (int) Math.max(Math.floor(accumulator/dt), 0);

                if(numSteps > 0)
                    accumulator -= numSteps * dt;
                numSteps = Math.min(numSteps, 5);
                //System.err.println(numSteps);
                isStepping = true;
                for(step = 0; step < numSteps; ++step)
                {
                    for(Body body = physicsWorld.getBodyList(); body != null; body = body.getNext())
                    {
                        for(Part p : body.getParts())
                        {
                            p.processControllers();
                            if(p.isMoving)
                                p.processMove();
                            if(p.getPosition().y < 0)
                            {
                                p.setPosition(new Vec2D(p.getPosition().x, 1));
                                p.setProp("Fixed", true, true);
                                p.getEvent("OutOfBounds").fire(Event.NULL);
                            }
                            else if(p.getPosition().x < 0)
                            {
                                p.setPosition(new Vec2D(1, p.getPosition().y));
                                p.setProp("Fixed", true, true);
                                p.getEvent("OutOfBounds").fire(Event.NULL);
                            }
                            else if(p.getPosition().x > width)
                            {
                                p.setPosition(new Vec2D(width - 1, p.getPosition().y));
                                p.setProp("Fixed", true, true);
                                p.getEvent("OutOfBounds").fire(Event.NULL);
                            }
                            else if(p.getPosition().y > height)
                            {
                                p.setPosition(new Vec2D(p.getPosition().x, height - 1));
                                p.setProp("Fixed", true, true);
                                p.getEvent("OutOfBounds").fire(Event.NULL);
                            }
                        }
                    }
                    physicsWorld.step(dt * timeScale * 5, physicsAccuracy);
                    Main.processStepTasks();
                }
                isStepping = false;
                frame++;
            }
            else
            {
                for(Body body = physicsWorld.getBodyList(); body != null; body = body.getNext())
                {
                    for(Part p : body.getParts())
                    {
                        p.processControllers();
                        if(p.isMoving)
                            p.processMove();
                    }
                }
                isStepping = true;
                physicsWorld.step(dt * timeScale * 5, physicsAccuracy);
                isStepping = false;
                Main.processStepTasks();
                frame++;
            }
            for(Body body = physicsWorld.getBodyList(); body != null; body = body.getNext())
            {
                body.clearForce();
                if(Float.isNaN(body.getPosition().x))
                    for(Part p : body.getParts())
                    {
                        p.remove();
                    }
            }
            for(GameObject obj : (ArrayList<GameObject>) steppableObjects.clone())
            {
                obj.step();
            }
            for(Body body = physicsWorld.getBodyList(); body != null; body = body.getNext())
            {
                body.m_old_xf.set(body.m_xf);
                body.m_oldRotation = body.getAngle();
            }
        }
    }
    public boolean isCriticalStep()
    {
        return step == numSteps - 1;
    }
    public float getTimestep()
    {
        return (float) dt;
    }
    public Vec2D getGravity()
    {
        return physicsWorld.getGravity();
    }
    public int getElapsedTime()
    {
        return elapsedTime;
    }
    /*public void addContactListener(ContactListener l)
    {
        physicsWorld.addContactListener(l);
    }
    public void removeContactListener(ContactListener l)
    {
        physicsWorld.removeContactListener(l);
    }*/
    public void setBackgroundColor(Color c)
    {
        backgroundColor = c;
    }
    public BufferedImage getBackgroundImage()
    {
        return backgroundImage;
    }
    public void setBackgroundImage(String path)
    {
        try {
            if(path.equals(""))
                backgroundImage = null;
            else
                backgroundImage = AssetManager.loadImage(path, path);
        } catch (IOException ex) {
            Main.log("Invalid background image URL");
        }
    }
    public void setTimescale(float t)
    {
        timeScale = t;
    }
    public void setMessage(String m)
    {
        message = m;
    }
    public String getMessage()
    {
        return message;
    }
    public void updateStats()
    {
        Stats.fps = fps;
        Stats.numBodies = physicsWorld.getBodyCount();
        Stats.numContacts = physicsWorld.getContactCount();
        Stats.numPairs = physicsWorld.getPairCount();
        Stats.numProxies = physicsWorld.getProxyCount();
        Stats.numJoints = physicsWorld.getJointCount();
        Stats.numObjects = objects.size();
        Stats.taskQueueSize = Main.getNumTasks();
    }
    public boolean shouldCollide(Shape shape1, Shape shape2)
    {
        return physicsWorld.m_contactFilter.shouldCollide(shape1, shape2);
    }
    public void connect(Part p1, Part p2, Weld w)
    {
        PartConnection c = new PartConnection();
        c.weld = w;
        c.part1 = p1;
        c.part2 = p2;
        if(!partConnections.contains(c))
            partConnections.add(c);
    }
    public void disconnect(Weld w)
    {
        Iterator<PartConnection> itr = partConnections.iterator();
        while(itr.hasNext())
        {
            PartConnection pc = itr.next();
            if(pc.weld == w)
                itr.remove();
        }
    }
    public boolean arePartsConnected(Part p1, Part p2)
    {
        PartConnection c = new PartConnection();
        c.part1 = p1;
        c.part2 = p2;
        return partConnections.contains(c);
    }
    public Part getPartAt(Vec2D pos)
    {
        for(int i = 2; i >= 0; i--)
        {
            List<GameObject> layer = getLayer(i);
            synchronized(layer)
            {
                for(int j = layer.size() - 1; j >= 0; j--)
                {
                    GameObject obj = layer.get(j);
                    if(!(obj instanceof Part))
                        continue;
                    Part p = (Part) obj;
                    if (p != null && !p.disabled && p.getShape().testPoint(((Part) obj).getBody().getXForm(), pos))
                        return p;
                }
            }
        }
        return null;
    }
    public TraceResult getTrace(Vec2D start, Vec2D unitVec, Part ignorePart)
    {
        boolean done = false;
        Vec2D pos = new Vec2D(start.x, start.y);
        Vec2D interval = unitVec.mul(0.25f);
        TraceResult t = null;
        while(!done)
        {
            if(pos.x < 0 || pos.x > width || pos.y < 0 || pos.y > height)
            {
                done = true;
                break;
            }
            pos.x += interval.x;
            pos.y += interval.y;
            Part p = getPartAt(pos);
            if(p != null && p != ignorePart)
            {
                t = new TraceResult();
                t.part = p;
                t.start = start;
                t.end = pos;
                t.distanceVec = pos.sub(start);
                done = true;
                break;
            }
        }
        return t;
    }
    public MultiTraceResult getMultiTrace(Vec2D start, Vec2D unitVec, Part ignorePart, double maxDistance)
    {
        boolean done = false;
        Vec2D pos = new Vec2D(start.x, start.y);
        Vec2D interval = unitVec.mul(0.25f);
        double d = interval.length();
        double distance = 0;
        MultiTraceResult m = new MultiTraceResult();
        ArrayList<TraceResult> results = new ArrayList<TraceResult>(WorldsSettings.MAX_OBJECTS);
        ArrayList<Part> parts = new ArrayList<Part>(WorldsSettings.MAX_OBJECTS);
        while(!done)
        {
            distance += d;
            if(distance > maxDistance)
            {
                done = true;
                break;
            }
            if(pos.x < 0 || pos.x > width || pos.y < 0 || pos.y > height)
            {
                done = true;
                break;
            }
            pos.x += interval.x;
            pos.y += interval.y;
            Part p = getPartAt(pos);
            if(!parts.contains(p))
            {
                parts.add(p);
                if(p != null && p != ignorePart)
                {
                    TraceResult t = new TraceResult();
                    t.part = p;
                    t.start = start;
                    t.end = pos;
                    t.distanceVec = pos.sub(start);
                    results.add(t);
                }
            }
        }
        TraceResult[] resultsArr = new TraceResult[results.size()];
        results.toArray(resultsArr);
        m.results = resultsArr;
        return m;
    }
    public void runScripts()
    {
        runScripts(Script.class);
        runScripts(HiddenScript.class);
    }
    private void runScripts(Class cl)
    {
        List<GameObject> scripts = getObjectsByType(cl);
        synchronized(scripts)
        {
            for (GameObject obj : scripts) {
                try {
                    if (obj.getBooleanProp("Enabled") && 
                        !(obj.getParent() instanceof Tool && (obj.getParent().getParent() instanceof StarterInventory || obj.getParent().getParent() instanceof Inventory))) {
                        ((Script) obj).run();
                    }
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                } catch (WorldsException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
    public void setDefaultCharacter(Part part) throws WorldsException
    {
        if(getService("Storage").hasChild("DefaultCharacter"))
            getService("Storage").getChild("DefaultCharacter").remove();
        net.worlds.object.Character c = (net.worlds.object.Character) createObject("Character");
        Part p = (Part) part.clone();
        c.setProp("Name", "DefaultCharacter", true);
        c.setProp("Visible", false, true);
        p.setProp("Name", "Body", true);
        VelocityController v = (VelocityController) createObject("VelocityController");
        v.setProp("Name", "Controller", true);
        v.setProp("UseRelativeVector", true, true);
        AngularVelocityController av = (AngularVelocityController) createObject("AngularVelocityController");
        av.setProp("Name", "RotationController", true);
        FixedRotation f = (FixedRotation) createObject("FixedRotation");
        f.setParent(p);
        v.setParent(p);
        av.setParent(p);
        p.setParent(c);
        c.setParent(getService("Storage"));
    }
}
